/*****************************************************************************
 *   Copyright 2005 Tim A Wang
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 ****************************************************************************/
package com.jb.web.bean;

import com.jb.faces.el.AttributeEvaluator;
import com.jb.framework.PersistentContext;
import com.jb.framework.PersistentManager;
import com.jb.util.Meter;
import com.jb.web.application.EventManager;
import com.jb.web.application.NWConstants;
import com.jb.web.application.configuration.Config;
import com.jb.web.bean.validator.ValidatorFactory;
import com.jb.web.context.WebContext;
import com.jb.web.renderer.DefaultRenderer;
import com.jb.web.renderer.RenderKitImpl;
import com.jb.web.renderer.RendererImpl;
import com.jb.web.validator.ValidatorImpl;

import java.util.ArrayList;
import java.util.Iterator;

import com.jb.faces.application.FacesMessage;
import com.jb.faces.component.UIComponent;
import com.jb.faces.context.FacesContext;
import com.jb.faces.render.Renderer;

import com.jb.web.model.Variable;

import javax.servlet.jsp.el.ELException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * UI implementation of base component.
 *
 * @author Tim Wang
 * @version 1.0
 */
public class UIComponentImpl extends BaseComponent {
    static final long serialVersionUID = 123123L;
   // public static final String JAVASCRIPT = "JavaScript";
    public static final String INTERNAL = "Internal";
    public static final String JS_HANDLER = "Client Event Handler";
    public static final String EVENT_HANDLER = "Event Handler";
    public static final String GENERAL = "General";
    public static final String SECURITY = "Security";
    public static final String STYLE = "Style";
    public static final String MODEL = "Model";
    public static final String UIDATA = "UI";
    
    public static final String DATA = "Model";
    
    public static final String EVENT = "EVENT";
//    public static final String META = "EVENT";

    static {
        //called when the component is loaded. This can be Java script
        publish(UIComponentImpl.class, EVENT_HANDLER, "onLoad");

        //display sequence of the component in its parent 
        publish(UIComponentImpl.class, GENERAL, "sequence");

        //client id, page unique id. Often used as id in client dom tree
        publish(UIComponentImpl.class, GENERAL, "clientId");

        //renderer to be used to render this component
        publish(UIComponentImpl.class, GENERAL, "rendererType");
    }

    protected Log log = LogFactory.getLog(getClass().getName());
    protected String visible = "true"; //publish

    //inidacates if this component is desiable
    protected boolean designable = false;
    protected boolean deletable = true;
    private ArrayList updateModelListners = new ArrayList();
    private ArrayList updateViewListners = new ArrayList();

    private Object modelBean;
    private Page myPage;

    //
    private String defaultValue;

    //life cycle event listner
    //hold the decoded value
    private Object decodedValue;
    
    private String onLoad; //script to run when loaded;
    private String onUpdateModel;
    private String onUpdateView;

    //class name of a default validator
    private String onValidate;

    //
    private String value;

    //inidicate if this is a facet or a child
    private boolean facet;
    private boolean personalizable;

    //this value is updated at 
    private int requestId = 0;

    public UIComponentImpl() {
        super();
    }
    //called when designer select this component
    //this should not be used by component users
    public void _onSelected() {
        //logic when designer selected this element
    }

    /**
     * Tests if a child component can be added as a child.
     *
     * @param child to be added
     *
     * @return true if it is ok to add this component as a child.
     */
    public boolean isChildAccepatble(BaseComponent child) {
        return child!=null;
    }

    /**
     * Returns an UIComponent whose client Id matches the parameter.
     * This call only look at immediate
     * children. Not a recursive call. If not found, null is returnd.
     *
     * @param pClientId client id to match
     *
     * @return UICompoent whose client id matches the parameter.
     */
    public BaseComponent getChildByClientId(String pClientId) {
        Iterator c = getChildren().iterator();

        while (c.hasNext()) {
            BaseComponent child = (BaseComponent) c.next();

            if (child instanceof UIComponentImpl) {
                UIComponentImpl uic = (UIComponentImpl) child;

                if (pClientId.equals(uic.getClientId())) {
                    return child;
                }
            }
        }

        return null;
    }
    public BaseComponent getFacetOrChildByClientId(String pClientId) {
        Iterator c = this.getFacetsAndChildren();

        while (c.hasNext()) {
            BaseComponent child = (BaseComponent) c.next();

            if (child instanceof UIComponentImpl) {
                UIComponentImpl uic = (UIComponentImpl) child;

                if (pClientId.equals(uic.getClientId())) {
                    return child;
                }
            }
        }

        return null;
    }

    /**
     * Returns descendant component by unique name. getQName is used to compare.
     * @param qName of the component
     * @return descendant component, null if not found.
     */
    public UIComponentImpl getComponentByQName(String qName) {
        if (this.getQName().equals(qName)) {
            return this;
        }

        Iterator c = getFacetsAndChildren();

        while (c.hasNext()) {
            UIComponentImpl child = (UIComponentImpl) c.next();

            if (child.getQName().equals(qName)) {
                return child;
            } else {
                UIComponentImpl grandChild = child.getComponentByQName(qName);

                if (grandChild != null) {
                    return grandChild;
                }
            }
        }

        return null;
    }

    /**
     * Sets decoded value, normally called by renderer that implement decode
     * method.
     *
     * @param decodedValue new value
     */
    public void setDecodedValue(Object decodedValue) {
        this.decodedValue = decodedValue;
    }

    /**
     * Gets decoded value. Decoded value is set to null when view is restored.
     *
     * @return decoded value
     */
    public Object getDecodedValue() {
        return decodedValue;
    }

    /**
     * Sets default value. Default value is a design time value that can be assigned
     * to a component so that if runtime value is null, or empty, default value
     * will be used. Default value can be an expression.
     *
     * @param defaultValue new default value
     */
    public void setDefaultValue(String defaultValue) {
        this.defaultValue = defaultValue;
    }

    /**
     * Retrns literal default value.
     *
     * @return defaultValue
     */
    public String getDefaultValue() {
        return defaultValue;
    }

    /**
     * Tests if this component is designable.
     *
     * @return true if it is a designable component.
     */
    public boolean isDesignable() {
        if (myPage == null) {
            myPage = getPage();
        }

        return (myPage == null) ? false : myPage.designable;
    }

    /**
     * Returns error message associated with this component.
     * Only if context has been assigned.
     *
     * @return error message, null if not found
     */
    public String getError() {
        WebContext ctx = (WebContext) getFacesContext();

        if (ctx != null) {
            return ctx.getErrorMessage(this);
        }

        return null;
    }

    /**
     * Sets facet flag. Facet is a component that belongs to this component, but
     * different from children in terms of lifecycle processing.
     *
     * @param facet if this is a facet
     */
    public void setFacet(boolean facet) {
        this.facet = facet;
    }

    /**
     * Returns true if this is a facet.
     *
     * @return if it is a facet.
     */
    public boolean isFacet() {
        return facet;
    }
    /**
     * Assign a value object to this bean.
     * @param modelBean model for this bean.
     */
    public void setModelBean(Object modelBean) {
        this.modelBean = modelBean;
    }
    /**
     * Returns object value assigned to this bean.
     * @return model object.
     */
    public Object getModelBean() {
        return modelBean;
    }


    /**
     * Sets onload event listner.
     *
     * @param onLoad event listner.
     */
    public void setOnLoad(String onLoad) {
        this.onLoad = onLoad;
    }

    /**
     * Returns on load event listner.
     *
     * @return onload event listner.
     */
    public String getOnLoad() {
        return onLoad;
    }

    /**
     * Sets default update model event lisnter.
     *
     * @param pOnUpdateModel event lisnter.
     */
    public void setOnUpdateModel(String pOnUpdateModel) {
        onUpdateModel = pOnUpdateModel;
    }

    /**
     * Gets default event listner for updating model.
     *
     * @return default event listner
     */
    public String getOnUpdateModel() {
        return onUpdateModel;
    }

    /**
     * Sets default event listner for update view.
     *
     * @param pOnUpdateView default event listner.
     */
    public void setOnUpdateView(String pOnUpdateView) {
        onUpdateView = pOnUpdateView;
    }

    /**
     * Gets onUpdateView event listner.
     *
     * @return event listner for updating view.
     */
    public String getOnUpdateView() {
        return onUpdateView;
    }

    /**
     * Sets event listner for validation.
     *
     * @param pValidator event listner.
     */
    public void setOnValidate(String pValidator) {
        onValidate = pValidator;
    }

    /**
     * Gets event listner for validate this component.
     *
     * @return event listner for validation.
     */
    public String getOnValidate() {
        return onValidate;
    }

    /**
     * Returns page this component belongs to.
     *
     * @return page component of this component.
     */
    public Page getPage() {
        if (this instanceof Page) {
            return (Page) this;
        } else {
            UIComponentImpl pItem = (UIComponentImpl) getParent();

            if (pItem != null) {
                if (pItem.myPage != null) {
                    return pItem.myPage;
                } else {
                    return pItem.getPage();
                }
            } else {
                return null;
            }
        }
    }

    /**
     * Sets if this component is allowed for personalization.
     *
     * @param personalizable
     */
    public void setPersonalizable(boolean personalizable) {
        this.personalizable = personalizable;
    }

    /**
     * Returns true if this component is personalizable.
     *
     * @return personalizable.
     */
    public boolean isPersonalizable() {
        return personalizable && Config.getInstance().isPersonalizable();
    }

    /**
     * Returns unique id for this component for the application. This id must
     * be uniquely identify the component rendered to the client. It is used
     * to locate the component that invokes an action.
     *
     * @return String id of this component.
     */
    public String getQName() {
        return getRoot().getId() + ":" + getId() + ":" + requestId;
    }

    /**
     * Factory method for its renderer.
     *
     * @param ctx Faces context
     *
     * @return Renderer instance for rendering.
     
    public Renderer getRenderer(FacesContext ctx) {
        Renderer renderer = RenderKitImpl.getRenderer(this);
        return renderer;
    }
     */
    /**
     * Gets sibling by client id
     *
     * @param pClientId client id of sibling.
     *
     * @return sibling component, null if not found.
     */
    public BaseComponent getSiblingByClientId(String pClientId) {
        if ((getParent() == null) || (pClientId == null)) {
            return null;
        }

        Iterator c = getChildren().iterator();

        while (c.hasNext()) {
            BaseComponent child = (BaseComponent) c.next();

            if (child instanceof UIComponentImpl) {
                UIComponentImpl uic = (UIComponentImpl) child;

                if (pClientId.equals(uic.getClientId())) {
                    return child;
                }
            }
        }

        return null;
    }


    /**
     * Sets value property for this component.
     *
     * @param value for value property.
     */
    public void setValue(String value) {
        this.value = value;
    }

    /**
     * Sets value for the component, and call onValueChange event listners.
     *
     * @param ctx Context
     * @param value new value
     */
    public void setValue(WebContext ctx, String value) {
        if ((value != this.value) || ((value != null) && !value.equals(this.value))) {
            this.value = value;
            valueChanged(ctx);
        }
    }

    /**
     * Gets value property of this component.
     *
     * @return string value.
     */
    public String getValue() {
        return (value == null) ? defaultValue : value;
    }

    /**
     * Sets visibile property.
     *
     * @param newValue string value for visible. true/false
     */
    public void setVisible(String newValue) {
        visible = newValue;
    }

    /**
     * Gets visible value.
     *
     * @return "true" if visible.
     */
    public String getVisible() {
        return visible;
    }

    //this is called before renderering the respond tree
    /**
     * Whether this component to be rendered
     * @param ctx
     * @return false if the componnet is not rendered.
     */
    public boolean isVisible(WebContext ctx) {
        try {
            Object visible = AttributeEvaluator.evaluate(ctx, this, getVisible(), Boolean.class);

            if (visible instanceof String) {
                return visible.toString().equalsIgnoreCase("true");
            } else if (visible instanceof Boolean) {
                return ((Boolean) visible).booleanValue();
            }
        } catch (ELException e) {
           if(log.isErrorEnabled())
               log.error(this, e);
        }
        return false;
    }

    /**
     * Adds a component as its child.
     *
     * @param component the child to be added.
     *
     * @throws ChildTypeNotAllowedException if child is not accepted.
     */
    public void addChild(BaseComponent component) throws ChildTypeNotAllowedException {
        if(component instanceof Variable && !(this instanceof Page))
        {
            this.getPage().addChild(component);
            return;
        }
        if (isChildAccepatble(component)) {
            super.addChild(component);
        } else {
            throw new ChildTypeNotAllowedException(component.getClass() + ":" + component.toString());
        }
    }

    /**
     * Adds update model event listner.
     *
     * @param listnerClassName event listner class name;
     */
    public void addUpdateModelListner(String listnerClassName) {
        if (!updateModelListners.contains(listnerClassName)) {
            updateModelListners.add(listnerClassName);
        }
    }

    /**
     * Adds update view event listner.
     *
     * @param listnerClassName event listner class name;
     */
    public void addUpdateViewListner(String listnerClassName) {
        if (!updateViewListners.contains(listnerClassName)) {
            updateViewListners.add(listnerClassName);
        }

        updateViewListners.add(listnerClassName);
    }

    /**
     * Event method called when a component is pasted
     * @param pasted the component to be paseted.
     */
    public void onPaste(Object pasted) {
        //
    }

    /**
     * Event method when a component is restored
     *
     * @param ctx FacesContext when restore is called.
     */
    public void onRestored(FacesContext ctx) {
        super.onRestored(ctx);
        decodedValue = null;
    }

    /**
     * Called to process update stage of the lifecycle. OnUpdateModel listners
     * are called if assigned to a component.
     *
     * @param ctx Faces context for hanling the request.
     */
    public void processUpdates(FacesContext ctx) {
        super.processUpdates(ctx);
        updateModel(ctx);
    }

    //process validators registered for this component

    /**
     * Processes all validators assigned to this component. This is called
     * by lifecycle manager in phase validation.
     * @param ctx
     */
    public void processValidators(FacesContext ctx) {
        if ((onValidate != null) && (onValidate.length() > 0)) {
            if (onValidate.indexOf("${") != -1) {
                String result = EventManager.getInstance().processELEvent((WebContext) ctx, this, onValidate);

                if (result!=null && result.length()>0) {
                    ctx.addMessage(getClientId(ctx), new FacesMessage(result));
                }
            } else {
                ValidatorImpl validator = ValidatorFactory.getInstance().getValidator(onValidate);

                if (validator == null) {
                    ctx.addMessage(getClientId(ctx), new FacesMessage("No Validator found " + onValidate));
                } else {
                    try {
                        validator.validate(ctx, this, getDecodedValue());
                    } catch (Exception e) {
                        ctx.addMessage(getClientId(ctx), new FacesMessage("Validation failed " + e.getMessage()));
                    }
                }
            }
        }

        Iterator iter = getChildren().iterator();

        while (iter.hasNext()) {
            Object tempObj = iter.next();

            if (tempObj instanceof UIComponent) {
                ((UIComponent) tempObj).processValidators(ctx);
            }
        }
    }

    /**
     * Removes update model event listner.
     *
     * @param listnerClassName event listner class name;
     */
    public void removeUpdateModelListner(String listnerClassName) {
        updateModelListners.remove(listnerClassName);
    }

    /**
     * Remove update view event listner.
     *
     * @param listnerClassName event listner class name;
     */
    public void removeUpdateViewListner(String listnerClassName) {
        updateViewListners.remove(listnerClassName);
    }

    /**
     * Renders the component to the output stream. This is done by delegating
     * to its renderer. If no renderer is assgined, default renderer will be used.
     *
     * @param ctx faces context used to render the result.
     *
     * @return String buffer
     */
    public Object render(WebContext ctx) {
        Meter.start();
        Renderer renderer = RenderKitImpl.getRenderer(this);

        try {
            if (renderer == null) {
                ctx.addErrorMessage(this, "No renderer has been assigned for this component " + getClass().getName());

                return new DefaultRenderer().render(ctx, this);
            } else {
                try {
                    Object obj = renderer.render(ctx, this);

                    return obj;
                } catch (Throwable t) {
                    t.printStackTrace();

                    if (log.isErrorEnabled()) {
                        log.error(this, t);
                    }

                    ctx.addErrorMessage(this, "Failed to render component " + getClass().getName());

                    return new DefaultRenderer().render(ctx, this);
                }
            }
        } finally {
            Meter.stop("Render " + this.getClientId() + " - " + renderer.getClass());
        }
    }
    /**
     * String representation of this class.
     * @return string name.
     */
    public String toString() {
        return getClientId();
    }

    /**
     * Updates view before rendered back to the user. This is called
     * by lifecycle manager at the phase of prepare view. This call walk
     * the response tree. If you override this method, be sure call super
     * method as onUpdateView event listners are called.
     *
     * @param ctx
     */
    public void updateView(WebContext ctx) {
        if (!isVisible(ctx)) {
            return;
        }
        Meter.start();

        handleEvent(ctx, onUpdateView);

        Iterator iter = updateViewListners.iterator();

        //block refresh
        while (iter.hasNext()) {
            handleEvent(ctx, (String) iter.next());
        }

        if ((getOnLoad() != null) && (getOnLoad().length() > 0)) {
            try {
                String onLoad = (String)com.jb.faces.el.AttributeEvaluator.evaluate(ctx, this, getOnLoad());

                if ((onLoad != null) && (onLoad.length() > 0)) {
                    ctx.addOnLoadStmt(onLoad);
                }
            } catch (ELException e) {
                if(log.isErrorEnabled())
                log.error(this, e);
            }
        }
        updateChildrenView(ctx);
        Meter.stop("Updateview " + this.getClass().getName()+":"+this.getClientId());
    }

    /**
     * Overrides to save personal style.
     *
     * @param ctx Web context
     *
     * @return true if value changed
     */
    public boolean valueChanged(PersistentContext ctx) {
        boolean result = super.valueChanged(ctx);
        this.savePersonalPage((WebContext)ctx);
        return result;
    }
    //save personal page
    private void savePersonalPage(WebContext ctx) {
        try
        {
            if (ctx.isRunTime() && personalizable) {
                PersistentManager pm = ctx.getPersonalPM();
                if (pm != null) {
                    pm.save(this.getPage(), true);//save this page if personalized is enabled
                }
            }
        }catch(Exception e) {
            if(log.isFatalEnabled()) {
                log.error(this, e);
            }
        }
    }
    /**
     * Sets the component to be designable or not.
     * @param pDesignable
     */
    protected void setDesignable(boolean pDesignable) {
        designable = pDesignable;
    }

    /**
     * This methods return true if an event is originated from this component.
     * This is determined by a parameter from request that matches the QName of
     * this component.
     *
     * @param ctx
     * @return true if the event is originated from this component.
     */
    protected boolean isMyEvent(WebContext ctx) {
        if (ctx.getRequestComponent() == null) {
            return false;
        }
        
        if (ctx.getRequestComponent().getId().equals(getId()) ) {
            return true;
        }

        String[] sources = ctx.getRequest().getParameterValues(NWConstants.NW_EVENT_SOURCE);

        if (sources == null) {
            return false;
        }

        for (int i = 0; i < sources.length; i++) {
            if (getQName().equals(sources[i])) {
                return true;
            }
        }

        return false;
    }

    /**
     * Process event listner for a given event name.
     * @param ctx
     * @param eventName
     * @return true if event is handled, else return false.
     */
    protected boolean handleEvent(WebContext ctx, String eventName) {
        if ((eventName != null) && (eventName.length() > 0)) {
            EventManager.getInstance().handleEvent(ctx, this, eventName);

            return true; //event has been handled
        }

        return false; //event has not been handled
    }
    /**
     * Updates views
     * @param ctx runtime context.
     */
    protected void updateChildrenView(WebContext ctx) {
        Iterator iter = getChildren().iterator();

        while (iter.hasNext()) {
            Object tempObj = iter.next();
            if (tempObj instanceof UIComponentImpl) {
                ((UIComponentImpl) tempObj).updateView(ctx);
            }
        }
    }

    /**
     * Called by lifecycle manager at the phase of processUpdates. Event listner
     * onUpdateModel is called if the event is originated from this component.
     *
     * It also process all updateModelLisnters if registered.
     *
     * This method walks the request tree.
     *
     * @param ctx
     * @return false if event not handled.
     */
    protected boolean updateModel(FacesContext ctx) {
        WebContext webCtx = (WebContext) ctx;
        boolean result = false;
        Meter.start();


        if (webCtx.isRenderResponse) {
            return result;
        }

        if (isMyEvent(webCtx)) {
            if (onUpdateModel != null) {
                result = handleEvent(webCtx, onUpdateModel);
            }

            if (ctx.getRenderResponse()) {
                return result;
            }

            Iterator iter = updateModelListners.iterator();

            while (iter.hasNext()) {
                result = handleEvent(webCtx, (String) iter.next());
            }

            requestId++;
        }

        Meter.stop( this.getClass() + " updateModel " + this.getClientId());
        return result;
    }
    /**
     * Object level flag whether this object can be deleted.
     * @param deletable true if this object can be deleted.
     */
    public void setDeletable(boolean deletable) {
        this.deletable = deletable;
    }
    /**
     * Returns whether this object is deletable.
     * @return true if this object is deleted.
     */
    public boolean isDeletable() {
        return deletable;
    }
}
